<?php
declare(strict_types = 1);

namespace app\process;

use Workerman\Connection\AsyncTcpConnection;
use Workerman\Timer;
use support\Redis;

/**
 * GPS 网关 TCP 客户端
 * - 我方作为 TCP 客户端，连接 scgps.zhdkj.com.cn:17910 或 gps.zhdkj.net 17910
 * - 长连接登录后，被动接收网关推送的 JSON 数据包（UTF-8）
 * - 心跳：无（网关不要求）；重连：必须（网络抖动、维护等）
 *
 * 运行方式：
 *   php start.php start | restart
 *
 * 配置项（.env）：
 *   TCP_HOST=scgps.zhdkj.com.cn           // 目标域名
 *   TCP_PORT=17910                        // 目标端口
 *   TCP_USER=账号                         // 登录账号
 *   TCP_PASS=密码                         // 登录密码
 *   TCP_WORKERS=1                         // 客户端进程数量
 *   TCP_RECONNECT_DELAY=5                 // 重连间隔秒
 *   TCP_IDLE_TIMEOUT=60                   // 接收空闲超时（秒），超过则主动断开重连
 *   TCP_SEND_DELAY_MS=0                   // 登录报文发送延时（毫秒）
 *   TCP_EOL=NONE                          // 登录行尾策略 NONE|CRLF|LF（探针结论：NONE）
 *   TCP_EOL_ORDER=NONE,CRLF,LF            // 登录失败时重试行尾顺序（优先 NONE）
 *   TCP_DEBUG_LOG=1                       // 是否打印每行 JSON 的简要日志（1 开启，0 关闭）
 *   TCP_BUFFER_MAX_BYTES=1048576          // 缓冲区最大字节数（超限截断尾部）
 *   TCP_HEARTBEAT_SEC=0                   // 心跳间隔秒（0 关闭）
 *   TCP_RECONNECT_JITTER=1                // 重连是否加入抖动（1 开启，0 关闭）
 *   LOG_MAX_BYTES=5242880                 // 单个日志文件最大字节数
 *   LOG_MAX_FILES=3                       // 日志滚动保留的文件个数
 *   GPS_QUEUE_MAX=100000                  // Redis 列表上限，超限背压丢弃
 *
 * 说明：
 * - 登录协议为私有文本协议，这里示例为 "LOGIN|{user}|{pass}"，根据网关实际调整。
 * - 数据流为 NDJSON（每行一个 JSON）；解析失败计数入 Redis。
 */
class GpsTcp
{
    /** @var AsyncTcpConnection|null 当前 TCP 连接实例（可能为空，需判空） */
    private $conn = null;

    /** @var string 接收缓冲区（拼接碎片并按换行拆分） */
    private string $buffer = '';

    /** @var bool 是否已登录成功（决定是否解析业务 JSON） */
    private bool $loggedIn = false;

    /** @var int 登录行尾顺序索引（用于失败时轮换 NONE/CRLF/LF） */
    private int $eolIndex = 0;

    /** @var array 登录行尾顺序（来自 .env TCP_EOL_ORDER） */
    private array $eolOrder = [];

    // 厂家建议：定期重新登录维持链接（5-10分钟）。以下为支持所需的属性。
    private string $user               = '';
    private string $pass               = '';
    private int    $reloginIntervalSec = 0;             // 重新登录间隔（秒）
    private ?int   $reloginTimerId     = null;          // 定时器 ID（用于清理）
    private ?int   $heartbeatTimerId   = null;          // 心跳定时器 ID（用于清理）

    /** 启动入口（每个 worker 调用一次） */
    public function onWorkerStart(): void
    {
        // 读取 .env 配置（带默认值）
        $host                     = env('TCP_HOST', 'scgps.zhdkj.com.cn');        // 目标域名
        $port                     = (int)env('TCP_PORT', 17910);                  // 目标端口
        $user                     = (string)env('TCP_USER', '');                  // 登录账号
        $pass                     = (string)env('TCP_PASS', '');                  // 登录密码
        $this->user               = $user;                                        // 保存用于定期重新登录
        $this->pass               = $pass;
        $this->reloginIntervalSec = (int)env('TCP_RELOGIN_MINUTES', 7) * 60; // 重新登录间隔（默认 7 分钟）
        Redis::set('gps:relogin_interval_sec', $this->reloginIntervalSec);
        $this->eolOrder = array_map('trim', explode(',', (string)env('TCP_EOL_ORDER', 'NONE,CRLF,LF'))); // 行尾顺序
        $primaryEol     = (string)env('TCP_EOL', 'NONE');                                                // 首选行尾（探针结论：NONE）
        // 将首选 EOL 放到顺序开头（去重保持顺序）
        $this->eolOrder = array_values(array_unique(array_merge([$primaryEol], $this->eolOrder)));

        $reconnectDelay = (int)env('TCP_RECONNECT_DELAY', 5);           // 重连间隔秒
        $idleTimeout    = (int)env('TCP_IDLE_TIMEOUT', 60);             // 空闲超时（秒）
        $sendDelayMs    = (int)env('TCP_SEND_DELAY_MS', 0);             // 登录报文延时（毫秒）

        $address = "tcp://{$host}:{$port}";                              // 拼接目标地址
        echo "[TCP] Target={$address} EOL_PRIMARY={$primaryEol} EOL_ORDER=" . implode(',', $this->eolOrder) . "\n";
        try {
            Redis::set('gps:target', $address);
            Redis::set('gps:eol_primary', $primaryEol);
            Redis::set('gps:eol_order', implode(',', $this->eolOrder));
            Redis::set('gps:send_delay_ms', $sendDelayMs);
        } catch (\Throwable $e) {
        }
        $this->conn = new AsyncTcpConnection($address);                  // 创建异步连接

        // 连接建立后发送登录
        $this->conn->onConnect = function (AsyncTcpConnection $conn) use ($user, $pass, $sendDelayMs)
        {
            // 启动心跳（若配置）
            $hb = (int)env('TCP_HEARTBEAT_SEC', 0);
            if ($hb > 0 && $this->heartbeatTimerId === null) {
                $this->heartbeatTimerId = Timer::add($hb, function () use ($conn)
                {
                    try {
                        $conn->send('PING');
                        Redis::incr('gps:heartbeat_sent');
                    } catch (\Throwable $e) {
                        Redis::incr('gps:heartbeat_failed');
                    }
                },                                   [], true);
            }
            $eolName = $this->eolOrder[$this->eolIndex] ?? 'NONE';        // 当前使用的行尾策略名
            $eol     = $this->eolString($eolName);                        // 转换为真实字符串
            $login   = "LOGIN|{$user}|{$pass}";                           // 组装登录报文（不带行尾，厂家文档要求）
            echo "[TCP] Connected, send login WITHOUT EOL\n";             // 打印提示，便于追踪
            if ($sendDelayMs > 0) {                                      // 若设置延时发送，使用定时器
                Timer::add($sendDelayMs / 1000.0, function () use ($conn, $login)
                {
                    $conn->send($login);                                 // 延时后发送登录报文
                },         [], false);
            } else {
                $conn->send($login);                                     // 立即发送登录报文
            }
            // 记录 Redis 指标（最近连接时间）
            try {
                Redis::set('gps:last_connect_at', time());
                // 标记登录已发送与时间
                Redis::set('gps:login_status', 'sent');
                Redis::set('gps:last_login_at', time());
                // 连接已建立，清除上一条错误信息（若有）
                Redis::set('gps:last_error', '');
            } catch (\Throwable $e) {
            }
        };

        // 接收数据（UTF-8 文本），按行解析 JSON
        $this->conn->onMessage = function (AsyncTcpConnection $conn, $data) use ($idleTimeout)
        {
            // 调试：记录每次收到的原始数据片段（前 256 字节十六进制），便于确认是否有业务数据推送
            try {
                $rawLog = function_exists('runtime_path') ? runtime_path('tcp_raw.log') :
                        __DIR__ . '/../../runtime/tcp_raw.log';
                $hex    = strtoupper(bin2hex(substr((string)$data, 0, 256)));
                @file_put_contents($rawLog, sprintf("%s len=%d hex=%s\n", date('c'), strlen((string)$data), $hex),
                                   FILE_APPEND | LOCK_EX);
            } catch (\Throwable $e) {
            }

            // 统计字节与 UTF-8 清洗、日志滚动
            try {
                Redis::incrby('gps:bytes_in', strlen((string)$data));
            } catch (\Throwable $e) {
            }
            $clean = $this->toUtf8((string)$data);
            try {
                $rawLog = function_exists('runtime_path') ? runtime_path('tcp_raw.log') :
                        __DIR__ . '/../../runtime/tcp_raw.log';
                $hex    = strtoupper(bin2hex(substr($clean, 0, 256)));
                $this->appendLogRotate($rawLog, sprintf("%s len=%d hex=%s\n", date('c'), strlen($clean), $hex));
            } catch (\Throwable $e) {
            }

            $this->buffer .= $clean;                                     // 拼接缓冲区（防止分包）
            // 缓冲区上限（防御性），超限截断尾部
            $maxBuf = max(1024, (int)env('TCP_BUFFER_MAX_BYTES', 1024 * 1024));
            if (strlen($this->buffer) > $maxBuf) {
                $this->buffer = substr($this->buffer, -$maxBuf);
                try {
                    Redis::incr('gps:buffer_trunc');
                } catch (\Throwable $e) {
                }
            }
            try {
                Redis::set('gps:last_packet_at', time());
            } catch (\Throwable $e) {
            }
            try {
                Redis::set('gps:buffer_size', strlen($this->buffer));
            } catch (\Throwable $e) {
            }

            // 首次登录响应可能无换行，做前置尝试（例如 "Login success"）
            if (!$this->loggedIn && $this->mayContainLoginResp($this->buffer)) {
                $this->handleLoginResponse($this->buffer);               // 尝试从缓存判断登录结果
            }
            // 若已登录但缓冲区前缀仍包含登录横幅/非JSON内容，丢弃到第一个 '{' 之前
            if ($this->loggedIn && $this->buffer !== '') {
                $this->buffer = preg_replace('/^[^{]*/', '', $this->buffer) ?? $this->buffer;
            }

            // 按行处理 NDJSON：查找换行位置，逐行截取
            while (false !== ($pos = strpos($this->buffer, "\n"))) {
                $line         = trim(substr($this->buffer, 0, $pos));            // 取出一行并去除首尾空格
                $this->buffer = substr($this->buffer, $pos + 1);                 // 剩余部分继续等待拆分
                if ($line === '') {                                      // 跳过空行
                    continue;
                }
                if (!$this->loggedIn) {                                  // 若尚未登录成功，优先识别登录响应
                    $this->handleLoginResponse($line);
                    continue;
                }
                $this->handleJsonOrArray($line);                         // 支持对象/数组帧
            }

            // 进一步增强：从缓冲区循环提取所有完整 JSON 帧（对象或数组），兼容无换行/粘包/多条合并/分片
            $extracted = 0;
            while (true) {
                $before = $this->buffer;
                if (!$this->extractAndHandleFrameFromBuffer()) break;
                $extracted++;
                if ($extracted > 100) { // 防御性上限
                    break;
                }
            }

            // 心跳开关：开启时定期发送轻量心跳（PING），帮助链路保活（上游不要求可关）
            $hb = (int)env('TCP_HEARTBEAT_SEC', 0);
            if ($hb > 0) {
                static $hbTimer = null;
                if ($hbTimer === null) {
                    $hbTimer = Timer::add($hb, function () use ($conn)
                    {
                        try {
                            $conn->send('PING');
                        } catch (\Throwable $e) {
                        }
                    },                    [], true);
                }
            }

            // 安排空闲重连：若长时间未收到数据，则主动断开让 onClose 重连
            $this->scheduleIdleReconnect($idleTimeout);
        };

        // 连接关闭：旋转行尾策略并重连
        $this->conn->onClose = function () use ($reconnectDelay)
        {
            echo "[TCP] Closed. loggedIn=" . ($this->loggedIn ? '1' :
                            '0') . " EOL=" . ($this->eolOrder[$this->eolIndex] ?? 'NONE') . "\n";
            if (!$this->loggedIn) {                                      // 登录失败时轮换行尾策略（NONE→CRLF→LF）
                $this->eolIndex = ($this->eolIndex + 1) % max(count($this->eolOrder), 1);
            }
            $this->buffer   = '';                                        // 清空缓冲区，避免旧数据污染
            $this->loggedIn = false;                                     // 重置登录状态
            // 清理重新登录定时器（避免在断开期间继续发送）
            if ($this->reloginTimerId !== null) {
                Timer::del($this->reloginTimerId);
                $this->reloginTimerId = null;
            }
            if ($this->heartbeatTimerId !== null) {
                Timer::del($this->heartbeatTimerId);
                $this->heartbeatTimerId = null;
            }
            Timer::add($reconnectDelay, function ()
            {                    // 延时发起重连（避免过快重试）
                $this->reconnect();
            },         [], false);
            try {
                Redis::incr('gps:tcp_closed');                  // 统计关闭次数
                Redis::set('gps:last_close_at', time());        // 记录最近关闭时间
            } catch (\Throwable $e) {
            }
        };

        // 错误：记录并重连
        $this->conn->onError = function ($conn, $code, $msg) use ($reconnectDelay)
        {
            echo "[TCP] Error {$code}: {$msg}\n";                        // 打印错误码与信息
            Redis::set('gps:last_error', "error[{$code}]:{$msg}");
            $delay = $reconnectDelay;
            if ((int)env('TCP_RECONNECT_JITTER', 1) === 1 && $delay > 1) {
                $j     = max(1, (int)round($delay * 0.2));
                $delay = max(1, $delay + random_int(-$j, $j));
            }
            Timer::add($delay, function ()
            {
                $this->reconnect();
            },         [], false);
            try {
                Redis::incr('gps:tcp_errors');
            } catch (\Throwable $e) {
            }
        };

        // 建立连接（异步）
        $this->conn->connect();
        echo "[TCP] Connecting {$address} ...\n";                        // 打印连接目标
    }

    /** 主动重连（尽量安全判空） */
    private function reconnect(): void
    {
        if ($this->conn) {                                               // 确保连接实例存在
            try {
                $this->conn->reConnect(0);                               // 触发 Workerman 的重连（立即）
            } catch (\Throwable $e) {
                echo "[TCP] reconnect exception: {$e->getMessage()}\n";  // 捕获异常以免 worker 崩溃
            }
        }
    }

    /**
     * 安排空闲重连
     * - 若超过 idleTimeout 未收到任何消息，则主动 close，让 onClose 触发重连
     */
    private function scheduleIdleReconnect(int $idleTimeout): void
    {
        static $timerId = null;                                          // 使用静态变量记录当前定时器 ID
        if ($timerId !== null) {                                         // 已存在定时器则先删除，避免重复
            Timer::del($timerId);
            $timerId = null;
        }
        if ($idleTimeout > 0) {                                          // 只在配置了超时时才设置
            $timerId = Timer::add((float)$idleTimeout, function ()
            {
                $last = (int)Redis::get('gps:last_packet_at');  // 最近收包的时间戳
                if ($last > 0 && (time() - $last) >= (int)env('TCP_IDLE_TIMEOUT', 60)) {
                    echo "[TCP] Idle timeout reached, reconnect\n";      // 打印空闲超时提示
                    if ($this->conn) {
                        $this->conn->close();                            // 主动关闭触发重连流程
                    }
                }
            },                    [], false);
        }
    }

    /** 判断缓冲区是否可能承载登录响应（例如包含 login/success/ok 关键词） */
    private function mayContainLoginResp(string $buf): bool
    {
        $s = strtolower($buf);                                           // 转为小写便于查找关键词
        return (str_contains($s, 'login') || str_contains($s, 'success') || str_contains($s, 'ok'));
    }

    /**
     * 处理登录响应，设置登录状态与指标
     * - 探针结果：无行尾（NONE）时返回 "Login success"
     * - 若成功，后续 onMessage 将按 JSON 行处理数据
     */
    private function handleLoginResponse(string $raw): void
    {
        $s = strtolower(trim($raw));                                     // 统一小写并去除前后空白
        echo $s;
        // 成功：Login success / ok
        $ok = (str_contains($s, 'login success') || preg_match('/\bok\b/u', $s) === 1 || str_contains($s, 'success'));
        if ($ok) {
            $this->loggedIn = true;                                      // 标记已登录
            try {
                Redis::set('gps:login_ok_at', time());          // 记录登录成功时间
                Redis::set('gps:login_status', 'ok');           // 登录状态标记
            } catch (\Throwable $e) {
            }
            echo "[TCP] Login OK\n";                                     // 打印成功日志
            // 厂家建议：周期性重新登录维持链接（若尚未启动定时器且间隔>0）
            if ($this->reloginTimerId === null && $this->reloginIntervalSec > 0) {
                $this->reloginTimerId = Timer::add((float)$this->reloginIntervalSec, function ()
                {
                    if ($this->conn) {
                        $msg = "LOGIN|{$this->user}|{$this->pass}";     // 不带行尾登录报文
                        $this->conn->send($msg);
                        Redis::set('gps:last_relogin_at', time());
                        Redis::incr('gps:relogin_count');
                        Redis::set('gps:login_status', 'sent');
                    }
                },                                 [], true);
            }
            return;
        }

        // 失败与错误场景：厂家文案
        if (str_contains($s, 'login failed')) {
            try {
                Redis::incr('gps:login_fail');
            } catch (\Throwable $e) {
            }
            try {
                Redis::set('gps:last_error', 'login_failed');
            } catch (\Throwable $e) {
            }
            echo "[TCP] Login FAILED: {$raw}\n";
            return;
        }
        if (str_contains($s, 'invalid parameter')) {
            Redis::incr('gps:login_fail');
            Redis::set('gps:last_error', 'login_error:invalid_parameter');
            echo "[TCP] Invalid Parameter: {$raw}\n";
            return;
        }
        if (str_contains($s, 'invalid data format 1')) {
            Redis::incr('gps:login_fail');
            Redis::set('gps:last_error', 'login_error:invalid_data_format_1');
            echo "[TCP] Invalid data format 1: {$raw}\n";
            return;
        }
        if (str_contains($s, 'invalid data format 2')) {
            Redis::incr('gps:login_fail');
            Redis::set('gps:last_error', 'login_error:invalid_data_format_2');
            echo "[TCP] Invalid data format 2: {$raw}\n";
            return;
        }

        // 其他未识别的响应，视为登录未确认，记录原文方便排查
        Redis::incr('gps:login_fail');
        Redis::set('gps:last_error', 'login_error:unknown:' . substr($raw, 0, 200));
        echo "[TCP] Login UNKNOWN RESP: {$raw}\n";
    }

    /**
     * 处理单行 JSON（UTF-8 NDJSON）
     * - 成功解析后入队 Redis 列表 gps:json，供入库进程消费
     * - TCP_DEBUG_LOG=1 时打印简要日志：设备、时间、坐标
     */
    private function handleJsonLine(string $line): void
    {
        $arr = json_decode($line, true);                                 // 解码 JSON 行
        if (is_array($arr)) {
            // 可选调试日志：开启 TCP_DEBUG_LOG=1 时打印简要信息（设备/时间/坐标）
            if ((int)env('TCP_DEBUG_LOG', 0) === 1) {
                $dev = $arr['deviceNumber'] ?? ($arr['device_id'] ?? ''); // 设备编号字段（兼容两种命名）
                $tm  = $arr['time'] ?? ($arr['reported_at'] ?? '');       // 上报时间字段
                $lng = $arr['lng'] ?? ($arr['longitude'] ?? '');          // 经度字段
                $lat = $arr['lat'] ?? ($arr['latitude'] ?? '');           // 纬度字段
                echo "[TCP] JSON recv device={$dev} time={$tm} lng={$lng} lat={$lat}\n";
            }

            // 追加原始 JSON 行到本地日志文件（即使 Redis 异常也能从文件观察到数据入站）
            try {
                $logFile = function_exists('runtime_path') ? runtime_path('gps_json.log') :
                        __DIR__ . '/../../runtime/gps_json.log';
                $this->appendLogRotate($logFile, date('c') . ' ' . $line . "\n");
            } catch (\Throwable $e) {
                // 文件写入失败不影响主流程
            }

            // 入队给保存进程（Redis 列表，生产者 lPush，消费者 rPop 保持 FIFO）
            try {
                // 队列水位背压：超阈值则丢弃并计数
                $maxQ = max(0, (int)env('GPS_QUEUE_MAX', 100000));
                $len  = (int)(Redis::lLen('gps:json') ?? 0);
                if ($maxQ > 0 && $len >= $maxQ) {
                    Redis::incr('gps:dropped');
                } else {
                    Redis::lPush('gps:json', json_encode($arr, JSON_UNESCAPED_UNICODE));
                    Redis::incr('gps:recv_packets');
                    try {
                        Redis::set('gps:last_packet_at', time());
                    } catch (\Throwable $e) {
                    }
                }
            } catch (\Throwable $e) {
                echo "[TCP] Redis unavailable while enqueuing JSON: {$e->getMessage()}\n";
            }
        } else {
            try {
                \support\Redis::incr('gps:json_errors');
            } catch (\Throwable $e) {
            }
            echo "[TCP] JSON parse error line: {$line}\n";                // 打印出错行便于定位
        }
    }

    /**
     * 处理单行：支持 JSON 对象或数组帧
     */
    private function handleJsonOrArray(string $line): void
    {
        $trim = ltrim($line);
        if ($trim === '') return;
        if ($trim[0] === '[') {
            $arr = json_decode($trim, true);
            if (is_array($arr)) {
                foreach ($arr as $item) {
                    $this->handleJsonLine(json_encode($item, JSON_UNESCAPED_UNICODE));
                }
            } else {
                try {
                    \support\Redis::incr('gps:json_errors');
                } catch (\Throwable $e) {
                }
                echo "[TCP] JSON array parse error: {$line}\n";
            }
            return;
        }
        $this->handleJsonLine($trim);
    }

    /**
     * 从缓冲区提取完整 JSON 帧（对象或数组），成功返回 true 并推进缓冲
     */
    private function extractAndHandleFrameFromBuffer(): bool
    {
        $buf = ltrim($this->buffer);
        if ($buf === '' || ($buf[0] !== '{' && $buf[0] !== '[')) {
            return false; // 非 JSON 起始，暂不处理
        }
        $open  = $buf[0];
        $close = ($open === '{') ? '}' : ']';
        $depth = 0;
        $inStr = false;
        $prev  = '';
        $len   = strlen($buf);
        for ($i = 0; $i < $len; $i++) {
            $ch = $buf[$i];
            if ($inStr) {
                if ($ch === '"' && $prev !== '\\') {
                    $inStr = false;
                }
            } else {
                if ($ch === '"') {
                    $inStr = true;
                } elseif ($ch === $open) {
                    $depth++;
                } elseif ($ch === $close) {
                    $depth--;
                    if ($depth === 0) {
                        $json         = substr($buf, 0, $i + 1);
                        $this->buffer = substr($buf, $i + 1);
                        if (!$this->loggedIn) {
                            $this->handleLoginResponse($json);
                        } else {
                            $this->handleJsonOrArray($json);
                        }
                        return true;
                    }
                }
            }
            $prev = $ch;
        }
        return false; // 未形成完整帧
    }

    /** 追加日志并按大小滚动（基于 env LOG_MAX_BYTES=5242880 LOG_MAX_FILES=3） */
    private function appendLogRotate(string $path, string $content): void
    {
        $max  = max(1024 * 100, (int)env('LOG_MAX_BYTES', 5 * 1024 * 1024));
        $keep = max(1, (int)env('LOG_MAX_FILES', 3));
        @file_put_contents($path, $content, FILE_APPEND | LOCK_EX);
        clearstatcache(true, $path);
        $size = @filesize($path) ?: 0;
        if ($size <= $max) return;
        // 滚动：.2 -> .3 ... -> 最新 .1
        for ($i = $keep; $i >= 2; $i--) {
            $older = $path . '.' . ($i - 1);
            $newer = $path . '.' . $i;
            if (file_exists($older)) @rename($older, $newer);
        }
        @rename($path, $path . '.1');
    }

    /** UTF-8 清洗，忽略非法字节 */
    private function toUtf8(string $s): string
    {
        if ($s === '') return $s;
        if (function_exists('mb_convert_encoding')) {
            $r = @mb_convert_encoding($s, 'UTF-8', 'UTF-8');
            if ($r !== false) return $r;
        }
        if (function_exists('iconv')) {
            $r = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
            if ($r !== false) return $r;
        }
        return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $s) ?? $s;
    }

    /**
     * 行尾字符串转换
     * - NONE：无行尾（探针结论需要 NONE）
     * - LF：\n
     * - CRLF：\r\n
     */
    private function eolString(string $name): string
    {
        switch (strtoupper($name)) {
            case 'LF':
                return "\n";                                              // 仅换行
            case 'CRLF':
                return "\r\n";                                            // 回车 + 换行
            case 'NONE':
            default:
                return '';                                                // 无行尾
        }
    }
}